feat(wallet): phone signing (OpenLV remote signing) — re-targeted to main - #196
Merged
Conversation
Sign with a wallet on your phone over the Open Lavatory protocol (openlv.sh): main publishes signing jobs to a renderer-hosted openlv session (P2P WebRTC, end-to-end encrypted, per-request QR); the phone's answer is verified against the account record before use. - remote signer backend behind the getSigner seam (type 'remote'): personal_sign / eth_signTypedData_v4 with recover-and-compare verification (REMOTE_WRONG_ACCOUNT), stable REMOTE_* error codes mirroring the LEDGER_* registry, EIP-1193 rejection mapping - optional Signer.sendTransaction capability: phones broadcast themselves; transaction-service prefers it and best-effort verifies the reported tx's sender on-chain; coded device errors now pass through its catch unwrapped - renderer session broker (one openlv session per job, dual-purpose QR URL with the session secret in the fragment) + preload IPC bridge that only accepts responses from the chrome renderer - shared signing-utils for EIP-712 wire-payload construction (was duplicated across vault/ledger, now also used by remote) - vendored @openlv bundle (LGPL-3.0, regenerable via npm run vendor:openlv), lazy-loaded off the renderer boot path - integration test: real @openlv stack, both roles, against an in-test MQTT broker — handshake, encryption, signing round-trips, and a relay-sees-only-ciphertext assertion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add remote (phone) accounts to the wallet list and the sidebar flow to connect one: show an openlv QR, receive the phone's accounts over the encrypted P2P session (eth_requestAccounts), pick and name one, and switch to it. Works with the vault locked — the key stays on the phone. - identity-manager: addDeviceWallet generalizes the Ledger add path (shared validation/dedup/auto-naming via DEVICE_LABELS); addRemoteWallet + wallet:add-remote-wallet IPC; device-neutral key-guard messages - broker connectPhone(): local discovery job reusing the session/ teardown/cancel plumbing via a per-job respond seam - Connect Phone subscreen with QR/status/retry, account picker, and success step; wallet selector gains the entry point and per-type badges (Ledger/Phone) - shared device-account picker + inline-error helpers + activateAddedWallet extracted; connect-ledger migrated to them Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a phone account signs (dapp tx/sign, manual send, x402 payment),
a QR panel overlays the approval flow: scan with the phone, live
connection status, cancel, and a fresh-QR retry that supersedes the
session without dropping the pending request.
- wallet-utils: accountType/isDeviceAccount/deviceLabel generalize the
ledger-only helpers; signingButtonLabel and the unlock-gate bypass
(renamed bypassUnlockGateForDevice) now cover phone accounts, so all
four approval flows skip the vault gate and label buttons per device
- broker: job events carry kind ('signing'|'connect'); retryJob mints a
new QR for a live job with staleness-guarded attempts; aborted
attempts release their session immediately (Promise.race) instead of
parking on the SDK's long response timeout
- send/x402 copy adapts to the device type (Ledger vs phone)
- shared PHASE_STATUS_TEXT + generateScannableQr used by both QR UIs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The other half of the dual-purpose QR: phones without an openlv-native wallet open the bridge page (static, in-repo under bridge/, no backend) inside their wallet app's browser. It joins the session from the URL fragment — never sent to any server — and forwards freedom's requests to the wallet's window.ethereum behind a method allowlist, so the wallet's own confirmation UI still gates every signature. - bridge/: self-contained page + the same openlv bundle the renderer vendors (built once, copied; npm run bridge:serve for local dev) - FREEDOM_OPENLV_SIGNALING env override (preload nodeConfig → broker) so tests pin the signaling relay to a local broker - Playwright E2E: real Electron app with a pre-seeded locked vault, bridge page in a plain Chromium as the "phone" (fake window.ethereum signing with a test key in Node) — connects a phone account through the sidebar UI and round-trips a verified personal_sign through the remote signer, QR panel included, all offline (local MQTT + local WebRTC) - shared test/helpers/local-mqtt-broker.js for the jest protocol test and the E2E fixture Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
freedom.florianglatz.eth.limo hosts the bridge page for smoke testing; final hostname still TBD. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phone-side dapp sessions default to Ethereum mainnet regardless of the wallet's selected network, so remote sendTransaction landed on the wrong chain (found in the Gnosis smoke test). The chainId field inside eth_sendTransaction params is ignored by wallets — the chain is session state, switched explicitly like any dapp does. - remote signer ships an EIP-3085 chain descriptor with each eth_sendTransaction job (name/currency/explorer from the chain registry; public https RPC endpoints only — a user's local node config never leaves the machine) - broker pre-flights wallet_switchEthereumChain over the same session (same QR); unknown chain (4902) → wallet_addEthereumChain with the descriptor, then switch again; a declined switch fails the job before the tx is ever sent - QR panel shows a "confirm the network switch" status; bridge page allowlists the two wallet_* methods Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The freedom mobile wallet endpoint (swarm-mobile-ios, WP-R4a) runs the vendored openlv SDK inside a hidden WKWebView, and WKWebView refuses ES-module imports from file:// pages while the SDK hard-requires a secure context for crypto.subtle — so bundle-openlv.js now also emits bridge/openlv.iife.js (window.OpenLV, classic script) and refreshes the sibling iOS checkout's vendored copy when present. npm run openlv:ios-harness plays the desktop host role for the iOS XCTest suite: local aedes MQTT broker + a headless Chromium page running the same SDK (Node has no WebRTC) + an HTTP control surface on 127.0.0.1:8798, sending the signing-job sequence (eth_requestAccounts, wallet_switchEthereumChain pre-flight, personal_sign) and recording the responses. The Chromium mDNS ICE workaround moves to test/helpers/webrtc.js, shared with the remote-signing E2E fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each XCTest consumes one host session; GET /reset reloads the Chromium host page so the next test gets a fresh session and a clean exchange log instead of a consumed one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two findings from the freedom-mobile smoke test:
Freedom's signing jobs never send eth_requestAccounts (the account was
captured in an earlier connect session), and strict wallets — freedom
mobile's own in-tab provider included — reject signing from an origin
that never connected ("Connect first … isn't authorized"). The bridge
now connects lazily before the first signing request; lenient wallets
treat the extra connect as a no-op.
The page also offers "Open in Freedom app" via the freedom:// custom
scheme, so scanning the QR with the plain camera can hand the session
to the native endpoint today, before the universal-link claim on this
origin is deployed. On phones without the app the tap is a no-op and
every other path stays available.
Redeploy bridge/ to Swarm to take effect.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/reset?mode=tx makes the host send the desktop signing-job shape for a transaction (eth_requestAccounts → chain-switch pre-flight → eth_sendTransaction) instead of personal_sign, so the iOS suite can prove the wallet endpoint's self-built transaction actually mines on a local anvil chain — not just that it was signed and broadcast. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bridge page is a standalone static site deployed independently to Swarm on its own cadence — it now lives in github.com/solardev-xyz/freedom-bridge. Everything here that touched bridge/ points at a sibling checkout instead: serve-bridge.js (and through it the remote-signing E2E) resolves ../freedom-bridge with a FREEDOM_BRIDGE_DIR override, the iOS harness reads the esm bundle from the renderer vendor copy (byte-identical), and bundle-openlv.js syncs both sibling repos' vendored bundles from its one build so nothing can drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
serve-bridge.js threw at require time when the sibling checkout was missing, which failed the whole `npm run test:e2e` sweep for anyone without it. The availability check moves behind bridgeAvailable() and the spec skips itself with a clone hint instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The require was one directory short (resolved to src/test/... instead of the repo-root test/helpers/), so the suite failed to load — the CI red on PR #159. Six directories up, six dots of shame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The openlv mqtt signaling layer uses the browser WebSocket global, which Node only ships from v22 — CI runs Node 20, so the suite failed with "WebSocket is not defined" there while passing on dev machines. Polyfill from `ws`, which the local broker helper already depends on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Distributable builds now fail while BRIDGE_ORIGIN still points at the interim test deployment, so the pre-merge checklist item on #159 is enforced rather than remembered. FREEDOM_ALLOW_INTERIM_BRIDGE=1 overrides for local experiments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve wallet, signing, preload, and build conflicts while preserving OpenLV remote-device behavior alongside main's current safety checks and chain-data routing.
Replaces the interim test deployment (freedom.florianglatz.eth.limo) with the production bridge origin https://bridge.freedom.baby, where the audited freedom-bridge artifact (repo commit 0a34fe0) is now deployed over HTTPS. This is the maintainer pre-merge checklist item from #159; it also satisfies the dist build guard in scripts/build.js (which refuses to ship while the origin is the interim host). Deployed artifact sha256: index.html d3ec3bb67c22efe5b36f5e73861e476c48494189adfa1d74ff925f77ad82516b bridge.js e24b932e65d774d35e21b41b0f08f2c61ecf4c33d0aff507f646ed6cf78bb66c openlv.esm.js 48bfb8411dc8b1a880154d03806f3c779f1bca8c72d31dbd5efcbc56de8798c7
Sign with your phone: OpenLV remote signing
This was referenced Aug 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phone signing (OpenLV remote signing) — re-targeted to
mainLands the remote (phone) signing feature originally in #159. That PR was stacked on #149 (
feature/hardware-wallet) and its base was never re-targeted tomain, so merging it landed the feature on the (now-stale)feature/hardware-walletbranch rather thanmain. This PR brings the exact same feature ontomain.Since #149 is already in
mainand the branch hadmainmerged in, the diff here is only the openlv/remote-signing feature (47 files) — no #149 re-introductions.Production bridge is deployed and pinned:
BRIDGE_ORIGIN = https://bridge.freedom.baby(nginx + Let's Encrypt on the release host), serving the auditedfreedom-bridgeartifact (commit0a34fe0). This clears the dist build guard from #159.Full feature description, testing (60+ unit, real-openlv integration, offline E2E, real-device smoke tests), and design notes are in #159. CI re-runs here against
main.